/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 * @format
 */

"use strict";

/* eslint-disable no-bitwise */

// $FlowFixMe: not defined by Flow
const constants = require("constants");
const stream = require("stream");
var _require = require("events");
const EventEmitter = _require.EventEmitter;

const FLAGS_SPECS = {
  r: { mustExist: true, readable: true },
  "r+": { mustExist: true, readable: true, writable: true },
  "rs+": { mustExist: true, readable: true, writable: true },
  w: { truncate: true, writable: true },
  wx: { exclusive: true, truncate: true, writable: true },
  "w+": { readable: true, truncate: true, writable: true },
  "wx+": { exclusive: true, readable: true, truncate: true, writable: true }
};

const ASYNC_FUNC_NAMES = [
  "access",
  "close",
  "copyFile",
  "fstat",
  "lstat",
  "open",
  "read",
  "readdir",
  "readFile",
  "readlink",
  "realpath",
  "stat",
  "unlink",
  "write",
  "writeFile"
];

/**
 * Simulates `fs` API in an isolated, memory-based filesystem. This is useful
 * for testing systems that rely on `fs` without affecting the real filesystem.
 * This is meant to be a drop-in replacement/mock for `fs`, so it mimics
 * closely the behavior of file path resolution and file accesses.
 */
class MemoryFs {
  constructor(options) {
    _initialiseProps.call(this);
    this._platform = (options && options.platform) || "posix";
    this._cwd = options && options.cwd;
    this._pathSep = this._platform === "win32" ? "\\" : "/";
    this.reset();
    ASYNC_FUNC_NAMES.forEach(funcName => {
      const func = this[`${funcName}Sync`];
      this[funcName] = function() {
        for (
          var _len = arguments.length, args = Array(_len), _key = 0;
          _key < _len;
          _key++
        ) {
          args[_key] = arguments[_key];
        }
        const callback = args.pop();
        process.nextTick(() => {
          let retval;
          try {
            retval = func.apply(null, args);
          } catch (error) {
            callback(error);
            return;
          }
          callback(null, retval);
        });
      };
    });
  }

  reset() {
    this._nextId = 1;
    this._roots = new Map();
    if (this._platform === "posix") {
      this._roots.set("", this._makeDir(0o777));
    } else if (this._platform === "win32") {
      this._roots.set("C:", this._makeDir(0o777));
    }
    this._fds = new Map();
  }

  _makeDir(mode) {
    return {
      entries: new Map(),
      gid: getgid(),
      id: this._getId(),
      mode,
      uid: getuid(),
      type: "directory",
      watchers: []
    };
  }

  _getId() {
    return ++this._nextId;
  }

  _open(filePath, flags, mode) {
    if (mode == null) {
      mode = 0o666;
    }
    const spec = FLAGS_SPECS[flags];
    if (spec == null) {
      throw new Error(`flags not supported: \`${flags}\``);
    }
    var _spec$writable = spec.writable;
    const writable = _spec$writable === undefined ? false : _spec$writable;
    var _spec$readable = spec.readable;
    const readable = _spec$readable === undefined ? false : _spec$readable;
    const exclusive = spec.exclusive,
      mustExist = spec.mustExist,
      truncate = spec.truncate;
    var _resolve = this._resolve(filePath);
    let dirNode = _resolve.dirNode,
      node = _resolve.node,
      basename = _resolve.basename,
      dirPath = _resolve.dirPath;
    let nodePath;
    if (node == null) {
      if (mustExist) {
        throw makeError("ENOENT", filePath, "no such file or directory");
      }
      node = {
        content: Buffer.alloc(0),
        gid: getgid(),
        id: this._getId(),
        mode,
        uid: getuid(),
        type: "file",
        watchers: []
      };

      dirNode.entries.set(basename, node);
      nodePath = dirPath.concat([[basename, node]]);
      this._emitFileChange(nodePath.slice(), { eventType: "rename" });
    } else {
      if (exclusive) {
        throw makeError("EEXIST", filePath, "directory or file already exists");
      }
      if (node.type !== "file") {
        throw makeError("EISDIR", filePath, "cannot read/write to a directory");
      }
      if (truncate) {
        node.content = Buffer.alloc(0);
      }
      nodePath = dirPath.concat([[basename, node]]);
    }
    return this._getFd(filePath, {
      nodePath,
      node,
      position: 0,
      readable,
      writable
    });
  }

  _parsePath(filePath) {
    let drive;
    const sep = this._platform === "win32" ? /[\\/]/ : /\//;
    if (this._platform === "win32" && filePath.match(/^[a-zA-Z]:[\\/]/)) {
      drive = filePath.substring(0, 2);
      filePath = filePath.substring(3);
    }
    if (sep.test(filePath[0])) {
      if (this._platform === "posix") {
        drive = "";
        filePath = filePath.substring(1);
      } else {
        throw makeError(
          "EINVAL",
          filePath,
          "path is invalid because it cannot start with a separator"
        );
      }
    }
    return { entNames: filePath.split(sep), drive };
  }

  /**
   * Implemented according with
   * http://man7.org/linux/man-pages/man7/path_resolution.7.html
   */
  _resolve(filePath, options) {
    let keepFinalSymlink = false;
    if (options != null) {
      keepFinalSymlink = options.keepFinalSymlink;
    }
    if (filePath === "") {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    var _parsePath = this._parsePath(filePath);
    let drive = _parsePath.drive,
      entNames = _parsePath.entNames;
    if (drive == null) {
      const _cwd = this._cwd;
      if (_cwd == null) {
        throw new Error(
          `The path \`${filePath}\` cannot be resolved because no ` +
            "current working directory function has been specified. Set the " +
            "`cwd` option field to specify a current working directory."
        );
      }
      const cwPath = this._parsePath(_cwd());
      drive = cwPath.drive;
      if (drive == null) {
        throw new Error(
          "On a win32 FS, the options' `cwd()` must return a valid win32 " +
            "absolute path. This happened while trying to " +
            `resolve: \`${filePath}\``
        );
      }
      entNames = cwPath.entNames.concat(entNames);
    }
    checkPathLength(entNames, filePath);
    const root = this._getRoot(drive, filePath);
    const context = {
      drive,
      node: root,
      nodePath: [["", root]],
      entNames,
      symlinkCount: 0,
      keepFinalSymlink
    };

    while (context.entNames.length > 0) {
      const entName = context.entNames.shift();
      this._resolveEnt(context, filePath, entName);
    }
    const nodePath = context.nodePath;
    return {
      drive: context.drive,
      realpath: context.drive + nodePath.map(x => x[0]).join(this._pathSep),
      dirNode: (() => {
        const dirNode =
          nodePath.length >= 2
            ? nodePath[nodePath.length - 2][1]
            : context.node;
        if (dirNode == null || dirNode.type !== "directory") {
          throw new Error("failed to resolve");
        }
        return dirNode;
      })(),
      node: context.node,
      basename: nullthrows(nodePath[nodePath.length - 1][0]),
      dirPath: nodePath
        .slice(0, -1)
        .map(nodePair => [nodePair[0], nullthrows(nodePair[1])])
    };
  }

  _resolveEnt(context, filePath, entName) {
    const node = context.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    if (node.type !== "directory") {
      throw makeError("ENOTDIR", filePath, "not a directory");
    }
    const entries = node.entries;
    if (entName === "" || entName === ".") {
      return;
    }
    if (entName === "..") {
      const nodePath = context.nodePath;
      if (nodePath.length > 1) {
        nodePath.pop();
        context.node = nodePath[nodePath.length - 1][1];
      }
      return;
    }
    const childNode = entries.get(entName);
    if (
      childNode == null ||
      childNode.type !== "symbolicLink" ||
      (context.keepFinalSymlink && context.entNames.length === 0)
    ) {
      context.node = childNode;
      context.nodePath.push([entName, childNode]);
      return;
    }
    if (context.symlinkCount >= 10) {
      throw makeError("ELOOP", filePath, "too many levels of symbolic links");
    }
    var _parsePath2 = this._parsePath(childNode.target);
    const entNames = _parsePath2.entNames,
      drive = _parsePath2.drive;
    if (drive != null) {
      context.drive = drive;
      context.node = this._getRoot(drive, filePath);
      context.nodePath = [["", context.node]];
    }
    context.entNames = entNames.concat(context.entNames);
    checkPathLength(context.entNames, filePath);
    ++context.symlinkCount;
  }

  _getRoot(drive, filePath) {
    const root = this._roots.get(drive.toUpperCase());
    if (root == null) {
      throw makeError("ENOENT", filePath, `no such drive: \`${drive}\``);
    }
    return root;
  }

  _write(fd, buffer, offset, length, position) {
    const desc = this._getDesc(fd);
    if (!desc.writable) {
      throw makeError("EBADF", null, "file descriptor cannot be written to");
    }
    if (position == null) {
      position = desc.position;
    }
    const node = desc.node;
    if (node.content.length < position + length) {
      const newBuffer = Buffer.alloc(position + length);
      node.content.copy(newBuffer, 0, 0, node.content.length);
      node.content = newBuffer;
    }
    buffer.copy(node.content, position, offset, offset + length);
    desc.position = position + length;
    return buffer.length;
  }

  _getFd(filePath, desc) {
    let fd = 3;
    while (this._fds.has(fd)) {
      ++fd;
    }
    if (fd >= 256) {
      throw makeError("EMFILE", filePath, "too many open files");
    }
    this._fds.set(fd, desc);
    return fd;
  }

  _getDesc(fd) {
    const desc = this._fds.get(fd);
    if (desc == null) {
      throw makeError("EBADF", null, "file descriptor is not open");
    }
    return desc;
  }

  _emitFileChange(nodePath, options) {
    const fileNode = nodePath.pop();
    let filePath = fileNode[0];
    let recursive = false;

    for (const watcher of fileNode[1].watchers) {
      watcher.listener(options.eventType, filePath);
    }

    while (nodePath.length > 0) {
      const dirNode = nodePath.pop();
      for (const watcher of dirNode[1].watchers) {
        if (recursive && !watcher.recursive) {
          continue;
        }
        watcher.listener(options.eventType, filePath);
      }
      filePath = dirNode[0] + this._pathSep + filePath;
      recursive = true;
    }
  }
}
var _initialiseProps = function() {
  var _this = this;
  this.constants = constants;
  this.accessSync = (filePath, mode) => {
    if (mode == null) {
      mode = constants.F_OK;
    }
    const stats = this.statSync(filePath);
    if (mode == constants.F_OK) {
      return;
    }
    const filePathStr = pathStr(filePath);
    if ((mode & constants.R_OK) !== 0) {
      if (
        !(
          (stats.mode & constants.S_IROTH) !== 0 ||
          ((stats.mode & constants.S_IRGRP) !== 0 && stats.gid === getgid()) ||
          ((stats.mode & constants.S_IRUSR) !== 0 && stats.uid === getuid())
        )
      ) {
        throw makeError("EPERM", filePathStr, "file cannot be read");
      }
    }
    if ((mode & constants.W_OK) !== 0) {
      if (
        !(
          (stats.mode & constants.S_IWOTH) !== 0 ||
          ((stats.mode & constants.S_IWGRP) !== 0 && stats.gid === getgid()) ||
          ((stats.mode & constants.S_IWUSR) !== 0 && stats.uid === getuid())
        )
      ) {
        throw makeError("EPERM", filePathStr, "file cannot be written to");
      }
    }
    if ((mode & constants.X_OK) !== 0) {
      if (
        !(
          (stats.mode & constants.S_IXOTH) !== 0 ||
          ((stats.mode & constants.S_IXGRP) !== 0 && stats.gid === getgid()) ||
          ((stats.mode & constants.S_IXUSR) !== 0 && stats.uid === getuid())
        )
      ) {
        throw makeError("EPERM", filePathStr, "file cannot be executed");
      }
    }
  };
  this.closeSync = fd => {
    const desc = this._getDesc(fd);
    if (desc.writable) {
      this._emitFileChange(desc.nodePath.slice(), { eventType: "change" });
    }
    this._fds.delete(fd);
  };
  this.copyFileSync = function(src, dest) {
    let flags =
      arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
    const options = flags & constants.COPYFILE_EXCL ? { flag: "wx" } : {};
    _this.writeFileSync(dest, _this.readFileSync(src), options);
  };
  this.openSync = (filePath, flags, mode) => {
    if (typeof flags === "number") {
      throw new Error(`numeric flags not supported: ${flags}`);
    }
    return this._open(pathStr(filePath), flags, mode);
  };
  this.readSync = (fd, buffer, offset, length, position) => {
    const desc = this._getDesc(fd);
    if (!desc.readable) {
      throw makeError("EBADF", null, "file descriptor cannot be written to");
    }
    if (position != null) {
      desc.position = position;
    }
    const endPos = Math.min(desc.position + length, desc.node.content.length);
    desc.node.content.copy(buffer, offset, desc.position, endPos);
    const bytesRead = endPos - desc.position;
    desc.position = endPos;
    return bytesRead;
  };
  this.readdirSync = (filePath, options) => {
    let encoding;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      encoding = options.encoding;
    }
    filePath = pathStr(filePath);
    var _resolve2 = this._resolve(filePath);
    const node = _resolve2.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    if (node.type !== "directory") {
      throw makeError("ENOTDIR", filePath, "not a directory");
    }
    return Array.from(node.entries.keys()).map(str => {
      if (encoding === "utf8") {
        return str;
      }
      const buffer = Buffer.from(str);
      if (encoding === "buffer") {
        return buffer;
      }
      return buffer.toString(encoding);
    });
  };
  this.readFileSync = (filePath, options) => {
    let encoding, flag;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      encoding = options.encoding;
      flag = options.flag;
    }
    const fd = this._open(pathStr(filePath), flag || "r");
    const chunks = [];
    try {
      const buffer = Buffer.alloc(1024);
      let bytesRead;
      do {
        bytesRead = this.readSync(fd, buffer, 0, buffer.length, null);
        if (bytesRead === 0) {
          continue;
        }
        const chunk = Buffer.alloc(bytesRead);
        buffer.copy(chunk, 0, 0, bytesRead);
        chunks.push(chunk);
      } while (bytesRead > 0);
    } finally {
      this.closeSync(fd);
    }
    const result = Buffer.concat(chunks);
    if (encoding == null) {
      return result;
    }
    return result.toString(encoding);
  };
  this.readlinkSync = (filePath, options) => {
    let encoding;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      encoding = options.encoding;
    }
    filePath = pathStr(filePath);
    var _resolve3 = this._resolve(filePath, { keepFinalSymlink: true });
    const node = _resolve3.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    if (node.type !== "symbolicLink") {
      throw makeError("EINVAL", filePath, "entity is not a symlink");
    }
    if (encoding == null || encoding === "utf8") {
      return node.target;
    }
    const buf = Buffer.from(node.target);
    if (encoding == "buffer") {
      return buf;
    }
    return buf.toString(encoding);
  };
  this.realpathSync = filePath => {
    return this._resolve(pathStr(filePath)).realpath;
  };
  this.writeSync = (
    fd,
    bufferOrString,
    offsetOrPosition,
    lengthOrEncoding,
    position
  ) => {
    let encoding, offset, length, buffer;
    if (typeof bufferOrString === "string") {
      position = offsetOrPosition;
      encoding = lengthOrEncoding;
      buffer = Buffer.from(bufferOrString, encoding || "utf8");
    } else {
      offset = offsetOrPosition;
      if (lengthOrEncoding != null && typeof lengthOrEncoding !== "number") {
        throw new Error("invalid length");
      }
      length = lengthOrEncoding;
      buffer = bufferOrString;
    }
    if (offset == null) {
      offset = 0;
    }
    if (length == null) {
      length = buffer.length;
    }
    return this._write(fd, buffer, offset, length, position);
  };
  this.writeFileSync = (filePath, data, options) => {
    let encoding, mode, flag;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      encoding = options.encoding;
      mode = options.mode;
      flag = options.flag;
    }
    if (encoding == null) {
      encoding = "utf8";
    }
    if (typeof data === "string") {
      data = Buffer.from(data, encoding);
    }
    const fd = this._open(pathStr(filePath), flag || "w", mode);
    try {
      this._write(fd, data, 0, data.length);
    } finally {
      this.closeSync(fd);
    }
  };
  this.mkdirSync = (dirPath, mode) => {
    if (mode == null) {
      mode = 0o777;
    }
    dirPath = pathStr(dirPath);
    var _resolve4 = this._resolve(dirPath);
    const dirNode = _resolve4.dirNode,
      node = _resolve4.node,
      basename = _resolve4.basename;
    if (node != null) {
      throw makeError("EEXIST", dirPath, "directory or file already exists");
    }
    dirNode.entries.set(basename, this._makeDir(mode));
  };
  this.symlinkSync = (target, filePath, type) => {
    if (type == null) {
      type = "file";
    }
    if (type !== "file") {
      throw new Error("symlink type not supported");
    }
    filePath = pathStr(filePath);
    var _resolve5 = this._resolve(filePath);
    const dirNode = _resolve5.dirNode,
      node = _resolve5.node,
      basename = _resolve5.basename;
    if (node != null) {
      throw makeError("EEXIST", filePath, "directory or file already exists");
    }
    dirNode.entries.set(basename, {
      id: this._getId(),
      gid: getgid(),
      target: pathStr(target),
      mode: 0o666,
      uid: getuid(),
      type: "symbolicLink",
      watchers: []
    });
  };
  this.existsSync = filePath => {
    try {
      var _resolve6 = this._resolve(pathStr(filePath));
      const node = _resolve6.node;
      return node != null;
    } catch (error) {
      if (error.code === "ENOENT") {
        return false;
      }
      throw error;
    }
  };
  this.statSync = filePath => {
    filePath = pathStr(filePath);
    var _resolve7 = this._resolve(filePath);
    const node = _resolve7.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    return new Stats(node);
  };
  this.lstatSync = filePath => {
    filePath = pathStr(filePath);
    var _resolve8 = this._resolve(filePath, { keepFinalSymlink: true });
    const node = _resolve8.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    return new Stats(node);
  };
  this.fstatSync = fd => {
    const desc = this._getDesc(fd);
    return new Stats(desc.node);
  };
  this.createReadStream = (filePath, options) => {
    let autoClose, encoding, fd, flags, mode, start, end, highWaterMark;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      autoClose = options.autoClose;
      encoding = options.encoding;
      fd = options.fd;
      flags = options.flags;
      mode = options.mode;
      start = options.start;
      end = options.end;
      highWaterMark = options.highWaterMark;
    }
    let st = null;
    if (fd == null) {
      fd = this._open(pathStr(filePath), flags || "r", mode);
      process.nextTick(() => st.emit("open", fd));
    }
    const ffd = fd;
    const readSync = this.readSync;
    const ropt = {
      filePath,
      encoding,
      fd,
      highWaterMark,
      start,
      end,
      readSync
    };
    const rst = new ReadFileSteam(ropt);
    st = rst;
    if (autoClose !== false) {
      const doClose = () => {
        this.closeSync(ffd);
        rst.emit("close");
      };
      rst.on("end", doClose);
      rst.on("error", doClose);
    }
    return rst;
  };
  this.unlinkSync = filePath => {
    filePath = pathStr(filePath);
    var _resolve9 = this._resolve(filePath, { keepFinalSymlink: true });
    const basename = _resolve9.basename,
      dirNode = _resolve9.dirNode,
      dirPath = _resolve9.dirPath,
      node = _resolve9.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    if (node.type !== "file" && node.type !== "symbolicLink") {
      throw makeError("EISDIR", filePath, "cannot unlink a directory");
    }
    dirNode.entries.delete(basename);
    this._emitFileChange(dirPath.concat([[basename, node]]), {
      eventType: "rename"
    });
  };
  this.createWriteStream = (filePath, options) => {
    let autoClose, fd, flags, mode, start;
    if (typeof options !== "string" && options != null) {
      autoClose = options.autoClose;
      fd = options.fd;
      flags = options.flags;
      mode = options.mode;
      start = options.start;
    }
    let st = null;
    if (fd == null) {
      fd = this._open(pathStr(filePath), flags || "w", mode);
      process.nextTick(() => st.emit("open", fd));
    }
    const ffd = fd;
    const ropt = { fd, writeSync: this._write.bind(this), filePath, start };
    const rst = new WriteFileStream(ropt);
    st = rst;
    if (autoClose !== false) {
      const doClose = () => {
        this.closeSync(ffd);
        rst.emit("close");
      };
      rst.on("finish", doClose);
      rst.on("error", doClose);
    }
    return st;
  };
  this.watch = (filePath, options, listener) => {
    filePath = pathStr(filePath);
    var _resolve10 = this._resolve(filePath);
    const node = _resolve10.node;
    if (node == null) {
      throw makeError("ENOENT", filePath, "no such file or directory");
    }
    let encoding, recursive, persistent;
    if (typeof options === "string") {
      encoding = options;
    } else if (options != null) {
      encoding = options.encoding;
      recursive = options.recursive;
      persistent = options.persistent;
    }
    const watcher = new FSWatcher(node, {
      encoding: encoding != null ? encoding : "utf8",
      recursive: recursive != null ? recursive : false,
      persistent: persistent != null ? persistent : false
    });
    if (listener != null) {
      watcher.on("change", listener);
    }
    return watcher;
  };
};

class Stats {
  /**
   * Don't keep a reference to the node as it may get mutated over time.
   */
  constructor(node) {
    this._type = node.type;
    this.dev = 1;
    this.mode = node.mode;
    this.nlink = 1;
    this.uid = node.uid;
    this.gid = node.gid;
    this.rdev = 0;
    this.blksize = 1024;
    this.ino = node.id;
    this.size =
      node.type === "file"
        ? node.content.length
        : node.type === "symbolicLink"
          ? node.target.length
          : 0;
    this.blocks = Math.ceil(this.size / 512);
    this.atimeMs = 1;
    this.mtimeMs = 1;
    this.ctimeMs = 1;
    this.birthtimeMs = 1;
    this.atime = new Date(this.atimeMs);
    this.mtime = new Date(this.mtimeMs);
    this.ctime = new Date(this.ctimeMs);
    this.birthtime = new Date(this.birthtimeMs);
  }

  isFile() {
    return this._type === "file";
  }
  isDirectory() {
    return this._type === "directory";
  }
  isBlockDevice() {
    return false;
  }
  isCharacterDevice() {
    return false;
  }
  isSymbolicLink() {
    return this._type === "symbolicLink";
  }
  isFIFO() {
    return false;
  }
  isSocket() {
    return false;
  }
}

class ReadFileSteam extends stream.Readable {
  constructor(options) {
    const highWaterMark = options.highWaterMark,
      fd = options.fd;
    // eslint-disable-next-line lint/flow-no-fixme
    // $FlowFixMe: Readable does accept null of undefined for that value.
    super({ highWaterMark });
    this.bytesRead = 0;
    this.path = options.filePath;
    this._readSync = options.readSync;
    this._fd = fd;
    this._buffer = Buffer.alloc(1024);
    const start = options.start,
      end = options.end;
    if (start != null) {
      this._readSync(fd, Buffer.alloc(0), 0, 0, start);
    }
    if (end != null) {
      this._positions = { current: start || 0, last: end + 1 };
    }
  }

  _read(size) {
    let bytesRead;
    const _buffer = this._buffer;
    do {
      const length = this._getLengthToRead();
      const position = this._positions && this._positions.current;
      bytesRead = this._readSync(this._fd, _buffer, 0, length, position);
      if (this._positions != null) {
        this._positions.current += bytesRead;
      }
      this.bytesRead += bytesRead;
    } while (this.push(bytesRead > 0 ? _buffer.slice(0, bytesRead) : null));
  }

  _getLengthToRead() {
    const _positions = this._positions,
      _buffer = this._buffer;
    if (_positions == null) {
      return _buffer.length;
    }
    const leftToRead = Math.max(0, _positions.last - _positions.current);
    return Math.min(_buffer.length, leftToRead);
  }
}

class WriteFileStream extends stream.Writable {
  constructor(opts) {
    super();
    this.path = opts.filePath;
    this.bytesWritten = 0;
    this._fd = opts.fd;
    this._writeSync = opts.writeSync;
    if (opts.start != null) {
      this._writeSync(opts.fd, Buffer.alloc(0), 0, 0, opts.start);
    }
  }

  _write(buffer, encoding, callback) {
    try {
      const bytesWritten = this._writeSync(this._fd, buffer, 0, buffer.length);
      this.bytesWritten += bytesWritten;
    } catch (error) {
      callback(error);
      return;
    }
    callback();
  }
}

class FSWatcher extends EventEmitter {
  constructor(node, options) {
    super();
    this._listener = (eventType, filePath) => {
      const encFilePath =
        this._encoding === "buffer" ? Buffer.from(filePath, "utf8") : filePath;
      try {
        this.emit("change", eventType, encFilePath);
      } catch (error) {
        this.close();
        this.emit("error", error);
      }
    };
    this._encoding = options.encoding;
    this._nodeWatcher = {
      recursive: options.recursive,
      listener: this._listener
    };
    node.watchers.push(this._nodeWatcher);
    this._node = node;
    if (options.persistent) {
      this._persistIntervalId = setInterval(() => {}, 60000);
    }
  }
  close() {
    this._node.watchers.splice(this._node.watchers.indexOf(this._nodeWatcher));
    clearInterval(this._persistIntervalId);
  }
}

function checkPathLength(entNames, filePath) {
  if (entNames.length > 32) {
    throw makeError(
      "ENAMETOOLONG",
      filePath,
      "file path too long (or one of the intermediate " +
        "symbolic link resolutions)"
    );
  }
}

function pathStr(filePath) {
  if (typeof filePath === "string") {
    return filePath;
  }
  return filePath.toString("utf8");
}

function makeError(code, filePath, message) {
  const err = new Error(
    filePath != null
      ? `${code}: \`${filePath}\`: ${message}`
      : `${code}: ${message}`
  );

  err.code = code;
  err.errno = constants[code];
  err.path = filePath;
  return err;
}

function nullthrows(x) {
  if (x == null) {
    throw new Error("item was null or undefined");
  }
  return x;
}

function getgid() {
  return process.getgid != null ? process.getgid() : -1;
}

function getuid() {
  return process.getuid != null ? process.getuid() : -1;
}

module.exports = MemoryFs;
